package com.jfinal.server.undertow.loader;

import com.jfinal.server.undertow.PropExt;
import com.jfinal.server.undertow.UndertowConfig;
import com.jfinal.server.undertow.ssl.SslConfig;

import java.util.*;
import java.util.function.Consumer;
import java.util.stream.Collectors;

import static com.jfinal.server.undertow.UndertowConfig.FILE_NAME;

/**
 * UndertowConfigPropertyLoader
 */
public class UndertowConfigPropertyLoader implements IUndertowConfigLoader {
    static final String ACTIVE_PROFILES = "undertow.profiles.active";

    static final String DEV_MODE = "undertow.devMode";
    static final String PORT = "undertow.port";
    static final String HOST = "undertow.host";
    static final String CONTEXT_PATH = "undertow.contextPath";

    static final String RESOURCE_PATH = "undertow.resourcePath";
    static final String ALLOW_RESOURCE_CHANGE_LISTENERS = "undertow.allowResourceChangeListeners";

    static final String IO_THREADS = "undertow.ioThreads";
    static final String WORKER_THREADS = "undertow.workerThreads";

    static final String GZIP_ENABLE = "undertow.gzip.enable";
    static final String GZIP_LEVEL = "undertow.gzip.level";
    static final String GZIP_MIN_LENGTH = "undertow.gzip.minLength";

    static final String HTTP2_ENABLE = "undertow.http2.enable";

    static final String SESSION_TIMEOUT = "undertow.session.timeout";
    static final String SESSION_HOT_SWAP = "undertow.session.hotSwap";

	static final String HOT_SWAP_CLASS_PREFIX = "undertow.hotSwapClassPrefix";
	static final String SYSTEM_CLASS_PREFIX = "undertow.systemClassPrefix";

    // ssl 模式下 http 请求是否跳转到 https
    static final String HTTP_TO_HTTPS = "undertow.http.toHttps";
    // ssl 模式下 http 请求跳转到 https 使用的状态码
    static final String HTTP_TO_HTTPS_STATUS_CODE = "undertow.http.toHttpsStatusCode";
    // ssl 模式下是否关闭 http
    static final String HTTP_DISABLE = "undertow.http.disable";

    static final String SERVER_NAME = "undertow.serverName";

    static final String BUFFER_SIZE = "undertow.bufferSize";
    static final String DIRECT_BUFFERS = "undertow.directBuffers";

    private final PropExt properties;


    public UndertowConfigPropertyLoader(Properties properties) {
        this.properties = new PropExt(properties);
    }

    public static IUndertowConfigLoader byConfigFile() {
        return byConfigFile(null);
    }

    /**
     * 使用指定的配置文件初始化 UndertowConfig，该配置文件不存在则抛出异常
     * 配置文件存在时还会继续尝试其生产环境配置文件，规则是：
     * 1：当配置文件名是 abc.properties
     * 2：开发环境配置文件是 abc-dev.properties
     * 3：生产环境配置文件是 abc-prod.properties
     *
     * 注意：生产环境配置文件不存在时不抛出异常，便于支持 fatjar 模式下创建
     * config/abc-prod.properties 文件用于配置生产环境
     *
     * @param configFileNameWithoutExtension 不带后缀的文件名, 默认为undertow
     */
    public static IUndertowConfigLoader byConfigFile(String configFileNameWithoutExtension) {
        return new UndertowConfigPropertyLoader(loadProperties(configFileNameWithoutExtension));
    }

    public static Properties loadProperties(String configFileName) {
        final String finalConfigFileName;
        if (null == configFileName || configFileName.isEmpty()) {
            finalConfigFileName = FILE_NAME;
        } else {
            finalConfigFileName = configFileName;
        }


        final PropExt ret = new PropExt();

        if (FILE_NAME.equals(finalConfigFileName)) {
            // 尝试加载默认配置文件
            ret.appendIfExists(finalConfigFileName + UndertowConfig.FILE_TYPE);
        } else {
            // 指定配置文件不存在时抛出异常
            ret.append(finalConfigFileName + UndertowConfig.FILE_TYPE);
        }

        // 根据devMode动态加载对应配置
        final boolean devMode = ret.getBoolean(DEV_MODE, false);

        // 获取激活的配置
        final Collection<String> activeProfiles = new ArrayList<>();
        final Collection<String> profiles = getActiveProfiles(ret.get(ACTIVE_PROFILES));

        if (profiles.isEmpty()) {
            // 如果没有配置激活的配置，默认根据devMode选择性加载
            activeProfiles.add(devMode ? "dev" : "prod");
        } else {
            activeProfiles.addAll(profiles);
        }

        activeProfiles.stream()
                .map(item -> finalConfigFileName + "-" + item + UndertowConfig.FILE_TYPE)
                .forEach(ret::appendIfExists);


        final Properties properties = ret.getProperties();
        // devMode 参数只允许通过默认配置文件进行配置，防止出现死循环，如下：
        // undertow.properties -> devMode: true
        // undertow-dev.properties -> devMode: false
        // 或者
        // undertow.properties -> devMode: false
        // undertow-prod.properties -> devMode: true
        properties.setProperty(DEV_MODE, String.valueOf(devMode));
        return properties;
    }

    public static Collection<String> getActiveProfiles(String defaultConfig) {
        final Collection<String> profiles = new LinkedList<>();
        final Collection<String> temp = Arrays.asList(
                System.getProperty(ACTIVE_PROFILES),
                System.getenv(ACTIVE_PROFILES),
                defaultConfig
        );

        temp.stream().filter(property -> null != property && !property.trim().isEmpty()).findFirst().ifPresent(item ->
                Arrays.stream(item.trim().split(",")).map(String::trim).forEach(profiles::add));

        return profiles;
    }

    public static IUndertowConfigLoader bySystemProperties() {
        final Properties properties = UndertowConfigPropertyLoader.loadPropertiesFormSystem();
        return new UndertowConfigPropertyLoader(properties);
    }

    /**
     * 使用 System.getProperty(...) 加载命令行传入的 undertow.port 与 undertow.host 参数，
     * 因为这两个参数最有可能在运行项目时进行变动，这个功能可以免去文件里配置最需要变动的 port 与 host
     *
     * 使用示例：
     * java -Dundertow.port=8080 -Dundertow.host=0.0.0.0 -jar jfinal-club-release.jar
     *
     * 传参注意事项：
     * 1：传参规则由 java 命令行给定，与 jfinal undertow 项目完全无关
     * 2：传参以 "-D" 为前缀，并且该前缀与后方的参数名之间不能有空格
     * 3：参数名与参数值中间用等号字符分格，且等号前后不能空格
     */
    public static Properties loadPropertiesFormSystem() {
        final Properties properties = new Properties();

        final List<String> accessProperties = Arrays.asList(HOST, PORT,
                RESOURCE_PATH,
                IO_THREADS, WORKER_THREADS);

        accessProperties.forEach(item -> {
            String property = System.getProperty(item);

            if (null != property) {
                properties.setProperty(item, property);
            }
        });

        return properties;
    }


	// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -


    @Override
    public UndertowConfig load() {
        final UndertowConfig config = new UndertowConfig();

        return load(config);
    }

    @Override
    public UndertowConfig load(final UndertowConfig config) {
        initAllowResourceChangeListeners();

        consumeIfNotNull(properties.getBoolean(DEV_MODE), config::setDevMode);

        consumeIfNotNull(properties.getInt(PORT), config::setPort);
        consumeIfNotEmpty(properties.get(HOST), config::setHost);
        consumeIfNotEmpty(properties.get(CONTEXT_PATH), config::setContextPath);

        consumeIfNotEmpty(properties.get(RESOURCE_PATH), string ->
                processArrayValue(string).forEach(config::addResourcePath));

        consumeIfNotNull(properties.getInt(IO_THREADS), config::setIoThreads);
        consumeIfNotNull(properties.getInt(WORKER_THREADS), config::setWorkerThreads);

        consumeIfNotNull(properties.getBoolean(GZIP_ENABLE), config::setGzipEnable);
        consumeIfNotNull(properties.getInt(GZIP_LEVEL), config::setGzipLevel);
        consumeIfNotNull(properties.getInt(GZIP_MIN_LENGTH), config::setGzipMinLength);

        consumeIfNotNull(properties.getBoolean(HTTP2_ENABLE), config::setDevMode);

        consumeIfNotNull(properties.getInt(SESSION_TIMEOUT), config::setSessionTimeout);
        consumeIfNotNull(properties.getBoolean(SESSION_HOT_SWAP), config::setSessionHotSwap);


        consumeIfNotEmpty(properties.get(HOT_SWAP_CLASS_PREFIX), string ->
                processArrayValue(string).forEach(config::addHotSwapClassPrefix));

        config.setSslConfig(new SslConfig(properties));

        consumeIfNotNull(properties.getBoolean(HTTP_TO_HTTPS), config::setDevMode);
        consumeIfNotNull(properties.getInt(HTTP_TO_HTTPS_STATUS_CODE), config::setHttpToHttpsStatusCode);
        consumeIfNotNull(properties.getBoolean(HTTP_DISABLE), config::setHttpDisable);

        consumeIfNotEmpty(properties.get(SERVER_NAME), config::setServerName);

        consumeIfNotNull(properties.getInt(BUFFER_SIZE), config::setBufferSize);
        consumeIfNotNull(properties.getBoolean(DIRECT_BUFFERS), config::setDirectBuffers);

        return config;
    }


    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -


    private <T> void consumeIfNotNull(T value, Consumer<T> consumer) {
        if (null != value) {
            consumer.accept(value);
        }
    }

    private void consumeIfNotEmpty(String value, Consumer<String> consumer) {
        if (null != value && !value.isEmpty()) {
            consumer.accept(value);
        }
    }


    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -


    private Collection<String> processArrayValue(String value) {
        return processArrayValue(value, ",");
    }

    private Collection<String> processArrayValue(String value, String split) {
        if (null == value) {
            return Collections.emptyList();
        }

        return Arrays.stream(value.split(",")).map(String::trim).filter(item -> !item.isEmpty()).collect(Collectors.toList());
    }


    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -


    /**
     * 配置是否打开资源文件变动监听器，配置为 false 可解决项目资源文件过多所导致的启动过慢问题
     * <p>
     * 当项目中的资源文件过多时（例如有几百万张图片），undertow 启动会非常慢，通过如下配置可以解决：
     * undertow.allowResourceChangeListeners = false
     * <p>
     * 目前是针对 PathResourceManager.java 中的相关逻辑，通过设置 "io.undertow.disable-file-system-watcher"
     * 为 "true" 来解决的，如果将来这部分源码的逻辑有变，需要随之改变解决方案
     * <p>
     * 对于绝大多数项目无需关注该配置
     */
    private void initAllowResourceChangeListeners() {
        Boolean allowResourceChangeListeners = properties.getBoolean(ALLOW_RESOURCE_CHANGE_LISTENERS);
        if (allowResourceChangeListeners != null) {
            boolean disableFileSystemWatcher = !allowResourceChangeListeners;
            System.setProperty("io.undertow.disable-file-system-watcher", Boolean.toString(disableFileSystemWatcher));
        }
    }

}

